use std::sync::Arc;

use wither::{
    bson::{doc, oid::ObjectId, DateTime, Document},
    mongodb::options::{FindOneAndUpdateOptions, ReturnDocument},
    Model,
};

use crate::{
    errors::cons_error,
    mdb::{MClient, ModelEx, MDB},
    models::{
        club_log::ClubLogType,
        invite::{InviteTokenModel, InviteTokenType},
        user::{SimpleUser, SimpleUserModel, UserModel, UserRole},
    },
    AppError, AppResult, Config, Pagination, PaginationQuery,
};

use super::{club_service::ClubService, user_service::UserService};
#[allow(unused)]
#[derive(Debug, Clone)]
pub struct InviteService {
    config: Arc<Config>,
    user_service: Arc<UserService>,
    club_service: Arc<ClubService>,
    mdb: MDB,
    m_client: MClient,
}

impl InviteService {
    pub fn new(
        config: Arc<Config>,
        club_service: Arc<ClubService>,
        user_service: Arc<UserService>,
        mdb: MDB,
        m_client: MClient,
    ) -> Self {
        Self {
            config,
            club_service,
            user_service,
            mdb,
            m_client,
        }
    }

    /// ## invite_user_to_club
    /// 邀请用户加入社团
    pub async fn invite_user_to_club<O: Into<SimpleUser>, T: Into<ObjectId>, C: Into<ObjectId>>(
        &self,
        op: O,
        target_id: T,
        club_id: C,
    ) -> AppResult<UserModel> {
        let target_id: ObjectId = target_id.into();
        self.club_service
            .add_log(
                op.into(),
                ClubLogType::Invite,
                doc! {
                    "target_id":target_id
                },
            )
            .await?;

        let user = self
            .club_service
            .upsert_user_in_club(target_id, club_id, UserRole::Student as u8)
            .await?;
        Ok(user)
    }

    /// ## check_user_in_invite_token
    /// 检查用户是否在邀请的token中
    pub async fn check_user_in_invite_token<T: Into<ObjectId>, U: Into<ObjectId>>(
        &self,
        token_id: T,
        user_id: U,
    ) -> AppResult<bool> {
        Ok(InviteTokenModel::find_one(
            &*self.mdb.clone(),
            doc! {
                "_id":token_id.into(),
                "checked_users.id":user_id.into()
            },
            None,
        )
        .await?
        .is_some())
    }

    /// ## check_user_in_club_invite_token
    /// 检查用户是否在某个社团的邀请token中
    pub async fn check_user_in_club_invite_token<
        T: Into<ObjectId>,
        U: Into<ObjectId>,
        F: Into<Option<Document>>,
    >(
        &self,
        club_id: T,
        user_id: U,
        filter: F,
    ) -> AppResult<bool> {
        let filter: Option<Document> = filter.into();
        let mut filter = filter.unwrap_or_default();
        filter.insert("club_id", club_id.into());
        filter.insert("expired_time", doc! {"$gt":DateTime::now()});
        filter.insert("checked_users.id", user_id.into());
        Ok(InviteTokenModel::find_one(&*self.mdb.clone(), filter, None)
            .await?
            .is_some())
    }

    pub async fn create_invite_token<
        T: Into<SimpleUserModel>,
        E: Into<DateTime>,
        M: Into<u64>,
        TY: Into<InviteTokenType>,
        CI: Into<ObjectId>,
    >(
        &self,
        creator: T,
        max_count: M,
        expired_time: E,
        invite_type: TY,
        club_id: CI,
    ) -> AppResult<InviteTokenModel> {
        let mut model =
            InviteTokenModel::new(creator, max_count, expired_time, invite_type, club_id);
        model.save(&*self.mdb.clone(), None).await?;
        Ok(model)
    }
    /// ## add_user_to_invite_token
    /// 添加用户到邀请token中，也就是说用户确定接受此次邀请了。
    /// 当此方法返回未找到token时，可能为以下几种情况
    /// 1. token不存在
    /// 2. token过期了
    /// 3. 已经使用过token了
    /// 4. token的使用达到上限
    pub async fn add_user_to_invite_token<T: Into<ObjectId>, U: Into<SimpleUserModel>>(
        &self,
        token_id: T,
        user: U,
    ) -> AppResult<InviteTokenModel> {
        let coll = InviteTokenModel::collection(&*self.mdb.clone());
        let user: SimpleUserModel = user.into();
        let res = coll
            .find_one_and_update(
                doc! {
                    "_id":token_id.into(),
                    "expired_time":{
                        "$gt":DateTime::now(),
                    },
                    "$expr":{
                        "$lt":["$use_count","$max_count"]
                    },
                    "checked_users.id":{
                        "$ne":user.id,
                    }
                },
                doc! {
                    "$addToSet":{
                        "checked_users":user
                    },
                    "$inc":{
                        "use_count":1
                    }
                },
                FindOneAndUpdateOptions::builder()
                    .return_document(ReturnDocument::Before)
                    .build(),
            )
            .await?
            .and_then(|d| InviteTokenModel::instance_from_document(d).ok())
            .ok_or(AppError::ConsError(cons_error::MSG_MDB_NOT_FOUND))?;
        Ok(res)
    }

    pub async fn find_all_invite_tokens<W: Into<Option<Document>>>(
        &self,
        filter: W,
        query: &PaginationQuery,
    ) -> AppResult<Pagination<InviteTokenModel>> {
        let filter: Option<Document> = filter.into();
        let mut filter = filter.unwrap_or_default();
        filter.insert("valid", true);
        InviteTokenModel::find_and_pagination(self.mdb.clone(), filter, query).await
    }

    pub async fn find_one_invite_token<W: Into<Option<Document>>, T: Into<ObjectId>>(
        &self,
        invite_id: T,
        filter: W,
    ) -> AppResult<InviteTokenModel> {
        let filter: Option<Document> = filter.into();
        let mut filter = filter.unwrap_or_default();
        filter.insert("_id", invite_id.into());
        filter.insert("valid", true);
        InviteTokenModel::find_one(&*self.mdb.clone(), filter, None)
            .await?
            .ok_or(AppError::ConsError(cons_error::MSG_MDB_NOT_FOUND))
    }

    pub async fn delete_invite_token<T: Into<ObjectId>>(&self, invite_id: T) -> AppResult<bool> {
        let coll = InviteTokenModel::collection(&*self.mdb.clone());
        let res = coll
            .update_one(
                doc! {"_id":invite_id.into()},
                doc! {"$set":{"valid":false}},
                None,
            )
            .await?;
        Ok(res.modified_count > 0)
    }
}
